#!/usr/bin/env python
#coding=utf-8
"""
Author:         Xia Kai <xiaket@gmail.com>
Filename:       tests.py
Type:           Unit test module
Last modified:  2010-05-21 16:51

Description:
This file contains unit test cases for this project.
"""
import os
import time
import unittest

from models import T163UserBase, User, Follower, Status, DirectMessage
from models import SearchResult, SearchHit, UserSearchResult, UserSearchHit
from session import T163Session as Session
from utils import AuthenticationError, UserNotFound, IllegalCall


# This FAKE_USER should not be available as an 163 account.
FAKE_USER = "xiakai.nju@gmail.com"
FAKE_PASS = "fakepass"

# This should be a valid account, preferably, at your disposal. If you do not
# have another account, at least find some account that is following you. and
# you are not following. The following test case would assume that the 
# TEST_ACCOUNT is following the account that you give to __init__.
# TEST_ACCOUNT_ALT should be someone who is not following you.
TEST_ACCOUNT = "xiaket"
TEST_ACCOUNT_ALT = "zhangjiawei"

# This should be an invalid screen_name
FAKE_SCREEN_NAME = "aslfkqwlalsdlfalkw"

# This should be an invalid user/status id.
FAKE_ID = "398066"

# These are test message contents, variables with prefix FAKE are longer than 
# 163 characters, while variables with prefix TEST lay within the constraint.
FAKE_MESSAGE_EN = u"abcdefghijklmnopqrstuvwxyz "*7
TEST_MESSAGE_EN = u"abcdefghijklmnopqrstuvwxyz "*6
FAKE_MESSAGE_CN = u"中文消息长度测试 "*19
TEST_MESSAGE_CN = u"中文消息长度测试 "*18


class SessionTests(unittest.TestCase):
    """
    This class would test authentication related cases. In summary, we have:
        1. No username provided.
        2. Invalid username
        3. Invalid username/password pair.
        4. Invalid cookie file.
        5. Invalid cookie file/username pair.
        6. Valid cookie file removed after Session initilization.
    """

    def __init__(self, testname, username=None, password=None, fulltest=False):
        """
        Over default __init__ method to allow import of username and password.
        Setup username and passwords.

        Some tests are somehow dangerous, we do them with a fulltest flag.
        """
        super(SessionTests, self).__init__(testname)
        self.username = username
        self.password = password
        self.fulltest = fulltest
        if fulltest:
            print "performing full test."

    def session_init(self):
        """
        This function would test the __init__ method of Session.
        There sure be no cookie file both before and after this test case.
        """
        # If neither username nor cookiefile is given to __init__. An
        # AuthenticationError would no doubt be raised.
        self.assertRaises(AuthenticationError, Session)

        # Should raise an AuthenticationError if an invalid username is
        # provided.
        self.assertRaises(AuthenticationError, Session, username=FAKE_USER)

        # This should not raise Exceptions, since it is a valid
        # username/password pair.
        Session(username=self.username, password=self.password)

        # This should not raise Exceptions, even though neither username nor
        # password is provided.
        Session(cookiefile="%s.txt" % self.username)

        # Since we have a valid cookie file now, anything given as username or
        # password would be ignored, so the following example would work.
        Session(cookiefile="%s.txt" % self.username, username=FAKE_USER)

        # Remove existing cookie file before we continue. The cookie should
        # have been created before.
        os.remove("%s.txt" % self.username)

        # This would cause AuthenticationError, since the cookiefile provided
        # does not exist.
        self.assertRaises(
            AuthenticationError, 
            Session, 
            cookiefile="%s.txt" % FAKE_USER,
        )

        # This would work, since a fallback username/password scheme would
        # work. But this would save cookie to "%s.txt" % FAKE_USER, instead of
        # "%s.txt" % self.username. So we shall remove the cookiefile after 
        # this test.
        Session(
            cookiefile="%s.txt" % FAKE_USER, 
            username=self.username,
            password=self.password,
        )
        os.remove("%s.txt" % FAKE_USER)

        # This should raise AuthenticationError, since it is an invalid
        # username/password pair.
        # CAUTION: This is dangerous. Frequent test of the following test would
        # lock your valid account up.
        if self.fulltest:
            self.assertRaises(
                AuthenticationError, 
                Session, 
                username=self.username, 
                password=FAKE_PASS,
            )

    def relation_api(self):
        """
        This function would test the relationship related APIs.
        """
        # Initialize a session.
        session = Session(username=self.username, password=self.password) 

        ##########################
        # self.show_friendship   #
        #------------------------#
        # /friendships/show.json #
        ##########################

        # Calling Session.show_friendship(target_screen_name=FAKE_SCREEN_NAME)
        # should cause an exception.
        self.assertRaises(
            UserNotFound, 
            session.show_friendship, 
            target_screen_name=FAKE_SCREEN_NAME,
        )

        # This time, the target is valid, while the source_id is invalid. It
        # should also cause an exception.
        self.assertRaises(
            UserNotFound, 
            session.show_friendship, 
            source_id=FAKE_ID,
            target_screen_name=TEST_ACCOUNT,
        )

        # This time, no target is provided, this should cause another
        # exception.
        self.assertRaises(
            IllegalCall, 
            session.show_friendship, 
            source_id=FAKE_ID,
        )

        # This should work, giving the following relationship between
        # TEST_ACCOUNT and self.username.
        fo, foed = session.show_friendship(
            target_screen_name=TEST_ACCOUNT
        )
        self.assertTrue(
            foed,
            "TEST_ACCOUNT:%s should follow %s" % (TEST_ACCOUNT, self.username),
        )

        ##########################################
        # self.followers                         #
        #----------------------------------------#
        # /statuses/followers/{screen_name}.json #
        ##########################################

        # User current user's screen_name by default, so this should equal.
        self.assertEqual(
            [user.id for user in session.followers()],
            [user.id for user in session.followers(session.screen_name)],
        )
        # This should never return an empty list, since at least TEST_ACCOUNT
        # is following self.username. Items in the list should be an instance
        # of Follower.
        followers = session.followers(session.screen_name)
        self.assertTrue(isinstance(followers[0], Follower))

        # Test again, TEST_ACCOUNT should be following self.username
        self.assertTrue(TEST_ACCOUNT in [u.screen_name for u in followers])

        # This should raise an exception, for the screen_name is invalid.
        self.assertRaises(
            UserNotFound, 
            session.followers, 
            screen_name=FAKE_SCREEN_NAME,
        )
        # The above should work for both positional arguments and keyword
        # arguments.
        self.assertRaises(
            UserNotFound, 
            session.followers, 
            FAKE_SCREEN_NAME,
        )

        ##########################################
        # self.friends                           #
        #----------------------------------------#
        # /statuses/friends/{screen_name}.json   #
        ##########################################

        # This should give a list of Follower objects.
        friends = session.friends(TEST_ACCOUNT)
        self.assertTrue(isinstance(friends[0], Follower))
        # Since an almost identical API is well tested(I hope!) above, I see no
        # point repeating it here.

        ############################################
        # self.create_friendship                   #
        #------------------------------------------#
        # /friendships/create/{screen_name}.json   #
        ############################################

        # As before, this function is decorated with check_screen_name.
        # So we shall get a UserNotFound with an invalid screen_name.
        self.assertRaises(
            UserNotFound, 
            session.create_friendship, 
            FAKE_SCREEN_NAME,
        )

        # Follow yourself would get an IllegalCall exception.
        self.assertRaises(
            IllegalCall, 
            session.create_friendship, 
            session.screen_name,
        )

        # Before we continue, we shall follow 
        # This should work.
        session.create_friendship(TEST_ACCOUNT)

        ############################################
        # self.destroy_friendship                  #
        #------------------------------------------#
        # /friendships/destroy/{screen_name}.json  #
        ############################################

        # This should work.
        session.destroy_friendship(TEST_ACCOUNT)

        # It cannot be done twice:
        self.assertRaises(
            IllegalCall, 
            session.destroy_friendship, 
            TEST_ACCOUNT,
        )

        # Nor can we unfollow someone who do not exist.
        self.assertRaises(
            UserNotFound, 
            session.destroy_friendship, 
            FAKE_SCREEN_NAME,
        )

    def mail_api(self):
        """
        This function would test the direct message related APIs.
        """
        # Initialize a session.
        session = Session(username=self.username, password=self.password) 

        ############################################
        # self.new_direct_message                  #
        #------------------------------------------#
        # /direct_messages/new.json                #
        ############################################

        # This is an IllegalCall, since the receiver is invalid.
        self.assertRaises(
            IllegalCall, 
            session.new_direct_message, 
            FAKE_SCREEN_NAME,
            TEST_MESSAGE_CN,
        )

        # This is an IllegalCall, since the message is too long.
        self.assertRaises(
            IllegalCall, 
            session.new_direct_message, 
            TEST_ACCOUNT,
            FAKE_MESSAGE_CN,
        )

        # This is an IllegalCall, since the message is too long.
        self.assertRaises(
            IllegalCall, 
            session.new_direct_message, 
            TEST_ACCOUNT,
            FAKE_MESSAGE_EN,
        )

        # This is an IllegalCall, since you cannot send a mail to yourself.
        self.assertRaises(
            IllegalCall, 
            session.new_direct_message, 
            session.screen_name,
            TEST_MESSAGE_EN,
        )

        # This is an IllegalCall, since TEST_ACCOUNT_ALT is not following you.
        self.assertRaises(
            IllegalCall, 
            session.new_direct_message, 
            TEST_ACCOUNT_ALT,
            TEST_MESSAGE_EN,
        )

        # Finally, this should work.
        message = session.new_direct_message(TEST_ACCOUNT, TEST_MESSAGE_EN)
        self.assertTrue(isinstance(message, DirectMessage))

        # A direct message is sent to TEST_ACCOUNT, we shall retrieve the first
        # message in outbox and check if it is the same one.

        ############################################
        # self.sent_direct_messages                #
        #------------------------------------------#
        # /direct_messages/sent.json               #
        ############################################

        messages = session.sent_direct_messages()
        self.assertTrue(isinstance(messages[0], DirectMessage))
        self.assertEqual(messages[0].id, message.id)

        ############################################
        # self.destroy_direct_message              #
        #------------------------------------------#
        # /direct_messages/destroy/{id}.json       #
        ############################################

        # This is an illegal call, since the id specified is invalid.
        self.assertRaises(
            IllegalCall, 
            session.destroy_direct_message, 
            FAKE_ID,
        )

        removed_message = session.destroy_direct_message(message.id)
        self.assertEqual(removed_message.id, message.id)

        # This is an illegal call, since you cannot remove the same direct 
        # message twice. 
        self.assertRaises(
            IllegalCall, 
            session.destroy_direct_message, 
            message.id,
        )

        ############################################
        # self.direct_messages                     #
        #------------------------------------------#
        # /direct_messages.json                    #
        ############################################

        # If there are direct messages in your inbox, we shall try to make sure
        # that it is a DirectMessage instance.
        messages = session.direct_messages()
        if len(messages) != 0:
            self.assertTrue(isinstance(messages[0], DirectMessage))

    def search_api(self):
        """
        This function would test the search related APIs.
        """
        # Initialize a session.
        session = Session(username=self.username, password=self.password) 

        ############################################
        # self.search                              #
        #------------------------------------------#
        # /search.json                             #
        ############################################

        # This is an IllegalCall, since no query keyword is specified.
        self.assertRaises(
            TypeError, 
            session.search, 
        )

        # I hope I'm not a narcissist...
        search_result = session.search(TEST_ACCOUNT)
        self.assertTrue(isinstance(search_result, SearchResult))
        result = search_result.result[0]
        self.assertTrue(isinstance(result, SearchHit))

        ############################################
        # self.user_search                         #
        #------------------------------------------#
        # /1/user/search.json                      #
        ############################################

        search_result = session.user_search(TEST_ACCOUNT)
        self.assertTrue(isinstance(search_result, UserSearchResult))
        result = search_result.result[0]
        self.assertTrue(isinstance(result, UserSearchHit))

    def favorite_api(self):
        """
        This function would test favorite related APIs.
        """
        # Initialize a session.
        session = Session(username=self.username, password=self.password) 

        ############################################
        # self.create_favorite                     #
        #------------------------------------------#
        # /favorites/create/{id}.json              #
        ############################################

        # This is an IllegalCall, since the id is invalid.
        self.assertRaises(
            IllegalCall,
            session.create_favorite,
            FAKE_ID,
        )

        # We shall get a valid message id by looking for the first message in
        # some user's timeline.
        favorited_status = session.user_timeline(TEST_ACCOUNT)[0]

        # Now add this message as favourite.
        session.create_favorite(favorited_status.id)

        # Now we are ready to test the self.favorites API.

        ############################################
        # self.favorites                           #
        #------------------------------------------#
        # /favorites/{screen_name}.json            #
        ############################################

        # We shall find the first favorite message and compare the id.
        favorite_status = session.favorites()[0]
        
        self.assertTrue(isinstance(favorite_status, Status))
        self.assertEqual(favorite_status.id, favorited_status.id)
        
        # This is an UserNotFound, since the screen_name is invalid.
        self.assertRaises(
            UserNotFound,
            session.favorites,
            FAKE_SCREEN_NAME,
        )

        ############################################
        # self.destroy_favorite                    #
        #------------------------------------------#
        # /favorites/destroy/{id}.json             #
        ############################################

        # This is an illegal call, since the id specified is invalid.
        self.assertRaises(
            IllegalCall, 
            session.destroy_favorite, 
            FAKE_ID,
        )

        # This would work
        session.destroy_favorite(favorited_status.id)

    def status_api(self):
        """
        This function would test the status related APIs.
        """
        # Initialize a session.
        session = Session(username=self.username, password=self.password) 

        ############################################
        # self.update                              #
        #------------------------------------------#
        # /statuses/update.json                    #
        ############################################

        # This is an IllegalCall, since the message is too long.
        self.assertRaises(
            IllegalCall, 
            session.update, 
            FAKE_MESSAGE_CN,
        )

        # This is an IllegalCall, since the message is too long.
        self.assertRaises(
            IllegalCall, 
            session.update, 
            FAKE_MESSAGE_EN,
        )

        newstatus = session.update(TEST_MESSAGE_CN)
        self.assertTrue(isinstance(newstatus, Status))

        # TODO, when we have fully implemented reply and retweet, we have to
        # add more test case here.

        ################################################
        # self.user_timeline                           #
        #----------------------------------------------#
        # /statuses/user_timeline/{screen_name}.json   #
        ################################################

        # The following screen_name is invalid.
        self.assertRaises(
            UserNotFound, 
            session.user_timeline, 
            FAKE_SCREEN_NAME,
        )

        # Get the time line. I do not understand why it take so long to refresh
        # the timeline. 20 is not enough.

        # The following code would sometimes mysteriously fail.
        if self.fulltest:
            time.sleep(30)
            statuses = session.user_timeline()
            self.assertEqual(statuses[0].id, newstatus.id)

        ############################################
        # self.show_status                         #
        #------------------------------------------#
        # /statuses/show/{id}.json                 #
        ############################################

        # This is an IllegalCall, since the id is invalid.
        self.assertRaises(
            IllegalCall,
            session.show_status,
            FAKE_ID,
        )

        self.assertEqual(
            session.show_status(newstatus.id).text, 
            TEST_MESSAGE_CN.strip(),
        )

        ############################################
        # self.destroy_status                      #
        #------------------------------------------#
        # /statuses/destroy/{id}.json              #
        ############################################

        # This is IllegalCall, since the message id is invalid.
        self.assertRaises(
            IllegalCall, 
            session.destroy_status, 
            FAKE_ID,
        )
        # This should work
        session.destroy_status(newstatus.id)

        # Get the time line. I do not understand why it take so long to refresh
        # the timeline. 20 is not enough. 30 would work most of the time.

        # The following code would sometimes mysteriously fail.
        if self.fulltest:
            time.sleep(30)
            statuses = session.user_timeline()
            self.assertNotEqual(statuses[0].id, newstatus.id)

        ############################################
        # self.home_timeline                       #
        # self.mentions                            #
        #------------------------------------------#
        # /statuses/home_timeline.json             #
        # /statuses/mentions.json                  #
        ############################################

        # Not much can be done for home timeline and mentions.
        statuses = session.home_timeline()
        self.assertTrue(isinstance(statuses[0], Status))
        statuses = session.mentions()
        self.assertTrue(isinstance(statuses[0], Status))
